﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Sustainalytics.Caching;
using Sustainalytics.Caching.L;
using Sustainalytics.Entities;
using Sustainalytics.Claims.BL;

namespace Sustainalytics.Claims
{
    public class ClaimsL1Cache : L1Cache, IClaimsL1Cache
    {
        private readonly IClaimMapComparer _claimMapComparer;
        private readonly IUserMapComparer _userMapComparer;
        protected IClaimsL2Cache ClaimsL2Cache { get; private set; }

        public ClaimsL1Cache() : this(new AppMemoryCache(), new ClaimsL2Cache(), new ClaimMapComparer(), new UserMapComparer() )
        {
        }

        public ClaimsL1Cache (IAppCache cache, IClaimsL2Cache l2Cache, IClaimMapComparer claimMapComparer, IUserMapComparer userMapComparer)
                 :base(cache, l2Cache)
        {
                 _claimMapComparer = claimMapComparer;
                 _userMapComparer = userMapComparer;
        }

        public IEnumerable<Claim> GetClaims(Resource resource, string verb, List<ResourceParameters> parameters)
        {
            List<ResourceMap> resourceMaps = GetResourceClaims(resource);
            List<Claim> resourceClaims = new List<Claim>();

            foreach (ResourceMap resourceMap in resourceMaps)
            {
                if ((resourceMap.Resource.Name.ToLowerInvariant().Equals(resource.Name.ToLowerInvariant()))
                    && (resourceMap.Value.ToLowerInvariant().Equals(verb.ToLowerInvariant())) 
                    && ValidateParameters(parameters, resourceMap.Parameters))
                {
                    List<string> result = this.L1L2Proxy.GetSet(resourceMap.Permission).ToList();

                    foreach (string str in result)
                    {
                        resourceClaims.Add(ClaimFromJSON(str));
                    }
                }
            }
            
            return resourceClaims;
        }

        private bool ValidateParameters(List<ResourceParameters> requestParameters, List<ResourceParameters> resourceParameters)
        {
            if (resourceParameters == null)
                return true;
            else
            {
                var anonRequestParams = requestParameters.Select(x => new { x.Key, x.Value }).ToList().OrderByDescending(x => x.Key);

                var anonResourceParams = resourceParameters.Select(x => new { x.Key, x.Value }).ToList().OrderByDescending(x => x.Key);

                var commonParams = anonResourceParams.Intersect(anonRequestParams);

                var result = anonResourceParams.SequenceEqual(commonParams);

                return result;
            }
        }

        public IEnumerable<Claim> GetUserClaims(string user)
        {
            if (string.IsNullOrEmpty(user))
            {
                throw new ArgumentException("User parameter cannot be empty!");
            }

            return GetClaims(user);
        }

        private List<Claim> GetClaims(string key)
        {
            List<Claim> claims = new List<Claim>();
            List<string> jsonClaim = this.L1L2Proxy.GetSet(key).ToList();

            if (jsonClaim != null)
            {
                foreach (string value in jsonClaim)
                {
                    claims.Add(ClaimFromJSON(value));
                }

            }
            return claims;
        }

        private List<ResourceMap> GetResourceClaims(Resource resource)
        {
            List<string> jsonMap = this.L1L2Proxy.GetSet(resource.Name).ToList();
            List<ResourceMap> resourceMap = new List<ResourceMap>();

            if (jsonMap != null)
            {
                foreach (string value in jsonMap)
                {
                    resourceMap.Add(ResourceMapFromJSON(value));
                }

            }
            else
            {

            }

            return resourceMap;
        }

        public IEnumerable<ResourceMap> GetAllResourceMaps()
        {
            List<ResourceMap> claimMaps = new List<ResourceMap>();

            List<string> jsonClaim = this.L1L2Proxy.GetSet("ResourceMaps").Distinct().ToList();

            if (jsonClaim != null)
            {
                foreach (string value in jsonClaim)
                {
                    List<string> jsonResource = this.L1L2Proxy.GetSet(value).ToList();

                    foreach(string _value in jsonResource)
                    {
                        claimMaps.Add(ResourceMapFromJSON(_value));
                    }
                }

            }
            else
            {

            }

            return claimMaps;
        }

        public IEnumerable<ClaimMap> GetAllClaimMaps()
        {
            List<ClaimMap> claimMaps = new List<ClaimMap>();
            List<string> jsonClaim = this.L1L2Proxy.GetSet("ClaimMaps").ToList();

            if (jsonClaim != null)
            {
                foreach (string value in jsonClaim)
                {
                    claimMaps.Add(ClaimMapFromJSON(value));
                }

            }
            else
            {

            }

            return claimMaps;
        }

        public IEnumerable<Claim> GetPermissionClaims(string permissionName)
        {
            if (string.IsNullOrEmpty(permissionName))
            {
                throw new ArgumentException("permissionName parameter cannot be empty!");
            }

            return GetClaims(permissionName);
        }

        public void AddClaims(Entities.Claims claims)
        {
            if (claims == null)
            {
                throw new ArgumentException("Claims List cannot be empty!");
            }

            AddPermissionClaims(claims.ClaimsMap);
            AddUserClaims(claims.UserClaims);
        }

        private void AddPermissionClaims(List<ClaimMap> claimsMap)
        {
            if (claimsMap != null)
            {
                // Save Permission, Claim, Value
                List<KeyValuePair<string, string>> claimMapsNames = new List<KeyValuePair<string, string>>();

                var existentClaimMap = GetAllClaimMaps().ToList();
                bool removed = RemoveObsoletePermisions(claimsMap, existentClaimMap);
                bool updated = false;

                foreach (ClaimMap map in claimsMap)
                {
                    // Add Claim Map to the List of ClaimMaps
                    claimMapsNames.Add(new KeyValuePair<string, string>("ClaimMaps", JsonConvert.SerializeObject(map)));

                    updated |= UpdateClaimMap(map, existentClaimMap);
                }
                if (removed || updated)
                    SetToCache(claimMapsNames);
            }
        }

        private bool UpdateClaimMap(ClaimMap map, IEnumerable<ClaimMap> existentClaimMap)
        {
            List<Claim> newClaims;
            List<Claim> toRemoveClaims;

            var existentMap = existentClaimMap.Where(f => f.Permission == map.Permission).SingleOrDefault();
            if (existentMap == null)//new permission
            {
                newClaims = map.Claims;
                toRemoveClaims = new List<Claim>();
            }
            else
            {
                bool diffPermissions;
                if (!_claimMapComparer.Diff(map, existentMap, out diffPermissions, out newClaims, out toRemoveClaims))
                    return false; //no need to update: identical feature with claims
            }

            UpdateCache(map.Permission, newClaims.Select(claim => JsonConvert.SerializeObject(claim)).ToList(),
                toRemoveClaims.Select(claim => JsonConvert.SerializeObject(claim)).ToList());

            return true;
        }

        private bool RemoveObsoletePermisions(List<ClaimMap> newClaimMap, List<ClaimMap> oldClaimMap)
        {
            var newFeatures = newClaimMap.Select(x => x.Permission).ToList();
            var oldFeatures = oldClaimMap.Select(x => x.Permission).ToList();

            var featuresToDelete = oldFeatures.Except(newFeatures).ToList();
            featuresToDelete.ForEach(f => DeleteFromCache(f));
            return featuresToDelete.Any();
        }

        public void AddUserClaims(List<UserClaim> claims)
        {
            // Save User, Claims, Value
            if (claims != null)
            {
                foreach (UserClaim map in claims)
                {
                    // update the Claim Map for Account
                    UpdateUserClaims(map);
                }
            }
        }

        private bool UpdateUserClaims(UserClaim accountClaims)
        {
            List<Claim> newClaims;
            List<Claim> toRemoveClaims;

            var existentClaims = GetUserClaims(accountClaims.GetNameIdKey()).ToList();
            
            if (existentClaims.Any() == false)//new account
            {
                newClaims = accountClaims.Claims;
                toRemoveClaims = new List<Claim>();
            }
            else
            {
                var existentAccount = new UserClaim() {Id = accountClaims.Id, Account = accountClaims.Account, Claims = existentClaims};
                bool diffPermissions;
                if (!_userMapComparer.Diff(accountClaims, existentAccount, out diffPermissions, out newClaims, out toRemoveClaims))
                    return false; //no need to update: identical account with claims
            }

            UpdateCache(accountClaims.GetNameIdKey(), newClaims.Select(claim => JsonConvert.SerializeObject(claim)).ToList(),
                toRemoveClaims.Select(claim => JsonConvert.SerializeObject(claim)).ToList());

            return true;
        }

        public void AddResourceMap(List<ResourceMap> map)
        {
            List<KeyValuePair<string, string>> resourceMaps = new List<KeyValuePair<string, string>>();
            List<KeyValuePair<string, string>> resourceMapsNames = new List<KeyValuePair<string, string>>();
            // Create the ResourceMap Entity
            foreach (ResourceMap resourceMap in map)
            {
                resourceMaps.Add(new KeyValuePair<string, string>(resourceMap.Resource.Name, JsonConvert.SerializeObject(resourceMap)));

                // Add ResourceMap to the List of ResourceMaps
                resourceMapsNames.Add(new KeyValuePair<string, string>("ResourceMaps", resourceMap.Resource.Name));
            }

            SetToCache(resourceMaps);
            SetToCache(resourceMapsNames);
        }

        public void DeleteResourceMap(ResourceMap map)
        {
            this.L1L2Proxy.Remove(map.Permission);
        }

        public void DeleteUserClaims(string user, List<Claim> claims)
        {
            foreach (Claim claim in claims)
            {
                this.L1L2Proxy.Remove(user, JsonConvert.SerializeObject(claim));
            }
        }

        public void DeleteUser(string user)
        {
            DeleteFromCache(user);
        }
        private void DeleteFromCache(string key)
        {
            this.L1L2Proxy.Remove(key);
        }

        public IEnumerable<ResourceMap> GetUIResources(List<Claim> claims) // needs cleaning up
        {
            List<ResourceMap> allowedResources = new List<ResourceMap>();

            List<ResourceMap> uiMaps = GetAllResourceMaps().ToList();

            var annonymousUserClaims = claims.Select(x => new { x.Value, x.Type }).ToList().OrderByDescending(x => x.Type);

            foreach (ResourceMap map in uiMaps)
            {
                // Get Claims for each Resource
                List<string> jsonClaim = this.L1L2Proxy.GetSet(map.Permission).ToList();
                
                List<Claim> tempClaim = new List<Claim>();
                
                foreach (string value in jsonClaim)
                {
                    tempClaim.Add(ClaimFromJSON(value));
                }

                var annonymousResourceClaims = tempClaim.Select(x => new { x.Value, x.Type }).ToList().OrderByDescending(x => x.Type).Distinct();

                var intersect = annonymousUserClaims.Intersect(annonymousResourceClaims);

                // Compare with user claims
                var checker = annonymousResourceClaims.SequenceEqual(intersect);

                if (checker)
                {
                    // Add to allowed list if the user has all the claims
                    allowedResources.Add(new ResourceMap() { Parameters = map.Parameters, Resource = map.Resource, Value = map.Value });
                }

            }

            return allowedResources;
        }

        private void SetToCache(List<KeyValuePair<string, string>> values)
        {
            var map = (from val in values
                       group val by val.Key into valG
                       select new { Key = valG.Key, Values = valG.Select(v => v.Value).ToList() }).ToList();

            map.ForEach(el =>
            {
                this.L1L2Proxy.Remove(el.Key);
                this.L1L2Proxy.Set(el.Key, el.Values);
            });
        }

        private void UpdateCache(string key, List<string> newValues, List<string> toDeleteValues)
        {
            toDeleteValues.ForEach(v => this.L1L2Proxy.Remove(key, v));
            newValues.ForEach(v => this.L1L2Proxy.Add2Set(key, v));
        }


        #region JSON Privates
        private Claim ClaimFromJSON(string jsonClaim)
        {
            JObject claim = JObject.Parse(jsonClaim);

            return ClaimFromJSON(claim);
        }

        private Claim ClaimFromJSON(JObject claim)
        {
            return new Claim((string)claim["Type"], (string)claim["Value"], ClaimValueTypes.String, (string)claim["Issuer"]);
        }

        private ClaimMap ClaimMapFromJSON(string jsonClaim)
        {

            JObject claimMap = JObject.Parse(jsonClaim);
            var claimArr = (JArray)claimMap["Claims"];

            return new ClaimMap() { Id = (Guid)claimMap["Id"], Permission = (string)claimMap["Permission"], Claims = claimArr.Select(c => {
                return ClaimFromJSON((JObject)c);
            }).ToList() };
        }

        private ResourceMap ResourceMapFromJSON(string jsonClaim)
        {
            ResourceMap resourceMap = JsonConvert.DeserializeObject<ResourceMap>(jsonClaim);

            return resourceMap;
        }
        #endregion
        
        public void ClearMemoryCache()
        {
            this.L1L2Proxy.Clear();
        }
    }
}
